using System;
using System.Collections.Generic;
using System.Security.Principal;
using InfiniTec.Data;
using InfiniTec.DirectoryServices.Properties;
using InfiniTec.Expressions;
using InfiniTec.Threading;

namespace InfiniTec.DirectoryServices
{
    /// <summary>
    /// Extends the <see cref="ActiveDirectoryEntry"/> with properties unique to 
    /// ActiveDirectory users, groups and contacts
    /// </summary>
    public class ActiveDirectoryPrincipal : ActiveDirectoryEntry
    {
        private readonly Connection _Connection;
        private readonly string _Name;
        private NameHint _NameHint;

        /// <summary>
        /// Initializes a new instance of the <see cref="ActiveDirectoryPrincipal"/> class.
        /// </summary>
        /// <param name="name">The name of the element. This can be a distinguished name, nt4 account name, email address, legacy exchange distinguished name or a display name. In the latter case, Ambiguous name resolution is used to find the principal.</param>
        /// <param name="connection">The connection.</param>
        /// <remarks>
        /// This constructor is equivalent to the constructor <see cref="ActiveDirectoryPrincipal(string, NameHint, Connection)"/>.
        /// </remarks>
        public ActiveDirectoryPrincipal(string name, Connection connection)
        : this(name, NameHint.Auto, connection)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ActiveDirectoryPrincipal"/> class.
        /// </summary>
        /// <param name="objectId">The object id.</param>
        /// <param name="connection">The connection.</param>
        public ActiveDirectoryPrincipal(Guid objectId, Connection connection)
        :
        base(objectId, connection)
        {
            AddRequiredFields(SchemaMapping.PrincipalSchema);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ActiveDirectoryPrincipal"/> class.
        /// </summary>
        /// <param name="identifier">The identifier.</param>
        /// <param name="connection">The connection.</param>
        public ActiveDirectoryPrincipal(SecurityIdentifier identifier, Connection connection)
        : this(identifier.ToString(), connection)
        {
            AddRequiredFields(SchemaMapping.PrincipalSchema);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ActiveDirectoryPrincipal"/> class.
        /// </summary>
        /// <param name="baseItem">The base item.</param>
        public ActiveDirectoryPrincipal(Item baseItem)
        : base(baseItem)
        {
            if (baseItem == null)
            {
                throw new ArgumentNullException("baseItem");
            }

            AddRequiredFields(SchemaMapping.PrincipalSchema);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ActiveDirectoryPrincipal"/> class.
        /// </summary>
        /// <param name="name">The name of the principal.</param>
        /// <param name="hint">A valid <see cref="NameHint"/> specifying the type of the name paramter.</param>
        /// <param name="connection">The connection to use.</param>
        public ActiveDirectoryPrincipal(string name, NameHint hint, Connection connection)
        {
            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentNullException("name");
            }
            if (connection == null)
            {
                throw new ArgumentNullException("connection");
            }

            AddRequiredFields(SchemaMapping.PrincipalSchema);
            _Name = name;
            _NameHint = hint;
            _Connection = connection;
        }

        /// <summary>
        /// Gets the name of the account.
        /// </summary>
        /// <value>The name of the account.</value>
        public string NT4AccountName
        {
            get { return Properties.GetPropertyValue(WellknownProperties.Common.NT4AccountName); }
        }

        /// <summary>
        /// Gets the object sid.
        /// </summary>
        /// <value>The object sid.</value>
        public SecurityIdentifier ObjectSid
        {
            get
            {
                byte[] result = Properties.GetPropertyValue(WellknownProperties.Common.ObjectSid);
                return (result != null) ? new SecurityIdentifier(result, 0) : null;
            }
        }

        /// <summary>
        /// Gets the type of the principal.
        /// </summary>
        /// <value>The type of the principal.</value>
        public PrincipalType? PrincipalType
        {
            get { return Properties.GetPropertyValue(WellknownProperties.Common.SAMAccountType); }
        }

        /// <summary>
        /// Gets a read-only list containing the distinguished names of all direct groups where
        /// the principal is a direct member of.
        /// </summary>
        /// <value>A list containing the distinguished names of all direct groups where
        /// the principal is a direct member of.</value>
        public IList<string> DirectGroupMemberShip
        {
            get { return Properties.GetMultiValuedPropertyValue(WellknownProperties.GroupMembership.MemberOf); }
        }

        /// <summary>
        /// Gets a read-only list containing the security identifiers 
        /// of all groups (direct and nested) where the principal is a member of.
        /// </summary>
        /// <value>A read-only list containing the security identifiers 
        /// of all groups (direct and nested) where the principal is a member of.</value>
        public IList<SecurityIdentifier> CompleteGroupMembership
        {
            get { return GetAsSecurityIdentifierList(WellknownProperties.GroupMembership.TokenGroups); }
        }

        /// <summary>
        /// Gets a read-only list containing the security identifiers 
        /// of all global and universal groups (direct and nested) where the principal 
        /// is a member of.
        /// </summary>
        /// <value>A read-only list containing the security identifiers 
        /// of all global and universal groups (direct and nested) where the principal 
        /// is a member of.</value>
        public IList<SecurityIdentifier> GlobalAndUniversalGroupMembership
        {
            get { return GetAsSecurityIdentifierList(WellknownProperties.GroupMembership.GlobalAndUniversalTokenGroups); }
        }

        /// <summary>
        /// Gets a read-only list of security identifiers due to a transitive group membership 
        /// expansion operation on a given user or computer. Token groups cannot be 
        /// retrieved if a Global Catalog is not present to retrieve the transitive 
        /// reverse memberships.
        /// </summary>
        /// <value>The no GC acceptable group membership.</value>
        public IList<SecurityIdentifier> NoGCAcceptableGroupMembership
        {
            get { return GetAsSecurityIdentifierList(WellknownProperties.GroupMembership.NoGCAcceptableTokenGroups); }
        }

        /// <summary>
        /// Gets the async operation operation used to refresh the item.
        /// </summary>
        /// <returns>
        /// An async operation which can be used to refresh the item.
        /// </returns>
        public override AsyncOperation GetRefreshOperation()
        {
            return new AsyncOperation("Refresh", RefreshInternal(), OperationTracker, false, null);
        }

        private IEnumerable<OperationAction> RefreshInternal()
        {
            PrincipalResolver searcher = null;

            if (BaseItem == null && !string.IsNullOrEmpty(_Name))
            {
                if (_NameHint == NameHint.Auto)
                {
                    DetermineNameHint();
                }

                switch (_NameHint)
                {
                    case NameHint.DistinguishedName:
                        BaseItem = new Item(_Name, _Connection);
                        break;
                    case NameHint.ObjectGuid:
                        BaseItem = new Item(string.Format("<Guid={0}>", _Name), _Connection);
                        break;
                    case NameHint.LegacyExchangeDn:
                        searcher = GetSearcher(Expression.Equals(WellknownProperties.Common.LegacyExchangeDn, _Name));
                        break;
                    case NameHint.EmailAddress:
                        searcher = GetSearcher(Expression.Equals(WellknownProperties.User.ProxyAddresses, "smtp:" + _Name));
                        break;
                    case NameHint.NT4AccountName:
                    case NameHint.SecurityIdentifier:
                        Guid id = TranslateNtAccountNameOrSecurityIdentifier();
                        BaseItem = new Item(string.Format("<Guid={0}>", id), _Connection);
                        break;
                    case NameHint.AmbigousNameResolution:
                        searcher = GetSearcher(Expression.Equals(WellknownProperties.Special.AmbiguousNameResolution, _Name));
                        break;
                    default:
                        throw new NotSupportedException(Resources.NotSupportedException_InvalidNameHintFormat(_NameHint));
                }

                if (searcher != null)
                {
                    AsyncOperation operation = searcher.GetFindOperation();
                    yield return new NestedOperation(operation);

                    if (searcher.SearchResult.Count == 1)
                    {
                        BaseItem = searcher.SearchResult[0].BaseItem;
                    }
                    else
                    {
                        throw new AmbiguousIdentityException(Resources.AmbiguousIdentityException_TheSpecifiedNameIsNotUniqueWithinTheForest,
                                                             searcher.SearchResult);
                    }
                    yield break;
                }
            }

            PreRefresh();

            AsyncOperation op = BaseItem.GetRefreshOperation();

            yield return new NestedOperation(op);

            PostRefresh();
        }

        private PrincipalResolver GetSearcher(Expression expression)
        {
            var searcher = new PrincipalResolver(_Connection);

            searcher.Filter = expression.ToString(new LdapFormatter());
            searcher.FilterType = FilterType.SimpleLdapFilter;
            searcher.ResolveTypes = GetResolveTypes();

            return searcher;
        }

        /// <summary>
        /// Gets types of principals to resolve by this instance.
        /// </summary>
        /// <returns></returns>
        protected virtual ResolveTypes GetResolveTypes()
        {
            return ResolveTypes.User | ResolveTypes.Contacts;
        }

        private Guid TranslateNtAccountNameOrSecurityIdentifier()
        {
            var translator = new Translator(_Connection);

            translator.InputFormat = (_NameHint == NameHint.NT4AccountName) ? NameFormat.NT4AccountName : NameFormat.SidOrSidHistory;
            translator.OutputFormat = NameFormat.UniqueId;
            var results = new List<TranslationResult>(translator.Translate(_Name));

            if (results[0].Status == TranslationStatus.Success)
            {
                return new Guid(results[0].TranslatedName);
            }
            throw new IdentityNotMappedException(Resources.IdentityNotMappedExceptionTranslationFailedFormat(_Name));
        }

        private void DetermineNameHint()
        {
            switch (PrincipalInfo.DeterminePrincipalNameType(_Name))
            {
                case PrincipalNameType.DistinguishedName:
                    _NameHint = NameHint.DistinguishedName;
                    break;
                case PrincipalNameType.EmailAddress:
                    _NameHint = NameHint.EmailAddress;
                    break;
                case PrincipalNameType.Guid:
                    _NameHint = NameHint.ObjectGuid;
                    break;
                case PrincipalNameType.SecurityIdentifier:
                    _NameHint = NameHint.SecurityIdentifier;
                    break;
                case PrincipalNameType.LegacyExchangeDistinguishedName:
                    _NameHint = NameHint.LegacyExchangeDn;
                    break;
                case PrincipalNameType.Nt4AccountName:
                    _NameHint = NameHint.NT4AccountName;
                    break;
                default:
                    _NameHint = NameHint.AmbigousNameResolution;
                    break;
            }
        }

        private IList<SecurityIdentifier> GetAsSecurityIdentifierList(PropertyDefinition<byte[]> propertyDefinition)
        {
            var result = new List<SecurityIdentifier>();
            IList<byte[]> tokenGroups = Properties.GetMultiValuedPropertyValue(propertyDefinition);
            if (tokenGroups == null) return result.AsReadOnly();

            foreach (var group in tokenGroups)
            {
                result.Add(new SecurityIdentifier(group, 0));
            }
            return result.AsReadOnly();
        }

        /// <summary>
        /// Translates the sids (from the CompleteGroupMembership property for example) to a 
        /// more readable format
        /// </summary>
        /// <param name="sidsToTranslate">The sids to translate.</param>
        /// <param name="targetFormat">The target format.</param>
        /// <returns>The tranlated values</returns>
        [CLSCompliant(false)]
        public IEnumerable<TranslationResult> TranslateSids(IEnumerable<SecurityIdentifier> sidsToTranslate, NameFormat targetFormat)
        {
            var translator = new Translator(BaseItem.Connection);
            translator.InputFormat = NameFormat.SidOrSidHistory;
            translator.OutputFormat = targetFormat;

            var sids = new List<string>();
            foreach (SecurityIdentifier sid in sidsToTranslate)
            {
                sids.Add(sid.Value);
            }
            return translator.Translate(sids);
        }

        /// <summary>
        /// Opens the specified directory entry and returns either a <see cref="ActiveDirectoryUser"/> or
        /// <see cref="ActiveDirectoryGroup"/>.
        /// </summary>
        /// <param name="distinguishedName">The distinguished name of the object.</param>
        /// <param name="connection">The <see cref="Connection"/> to use.</param>
        /// <returns>An <see cref="ActiveDirectoryPrincipal"/>.</returns>
        public static ActiveDirectoryPrincipal Open(string distinguishedName, Connection connection)
        {
            var item = new ActiveDirectoryPrincipal(distinguishedName, connection);
            item.Refresh();

            if (item.ObjectClass.Contains(WellknownProperties.ObjectClasses.Group)) return new ActiveDirectoryGroup(item.BaseItem);
            if (item.ObjectClass.Contains(WellknownProperties.ObjectClasses.User)) return new ActiveDirectoryUser(item.BaseItem);

            throw new InvalidObjectCategoryException("The object doesn't have the proper object categories");
        }

        /// <summary>
        /// Opens the specified directory entry and returns either a <see cref="ActiveDirectoryUser"/> or
        /// <see cref="ActiveDirectoryGroup"/>.
        /// </summary>
        /// <param name="id">The Active Directory <see cref="System.Guid"/> of the object.</param>
        /// <param name="connection">The <see cref="Connection"/> to use.</param>
        /// <returns>An <see cref="ActiveDirectoryPrincipal"/>.</returns>
        public static ActiveDirectoryPrincipal Open(Guid id, Connection connection)
        {
            return Open(string.Format("<Guid={0}>", id), connection);
        }

        /// <summary>
        /// Opens the specified directory entry and returns either a <see cref="ActiveDirectoryUser"/> or
        /// <see cref="ActiveDirectoryGroup"/>.
        /// </summary>
        /// <param name="sid">The <see cref="SecurityIdentifier"/> of the object.</param>
        /// <param name="connection">The <see cref="Connection"/> to use.</param>
        /// <returns>An <see cref="ActiveDirectoryPrincipal"/>.</returns>
        public static ActiveDirectoryPrincipal Open(SecurityIdentifier sid, Connection connection)
        {
            return Open(string.Format("<Sid={0}>", sid), connection);
        }
    }
}